package drds.data_propagate.binlog_event.binlog_event.load_infile_replication;

import drds.data_propagate.binlog_event.BinLogEvent;
import drds.data_propagate.binlog_event.BinlogEventType;
import drds.data_propagate.binlog_event.Buffer;
import drds.data_propagate.binlog_event.binlog_event.Header;
import drds.data_propagate.binlog_event.binlog_event.binlog_management.FormatDescriptionEvent;
import lombok.Getter;
import lombok.Setter;

/**
 * This log binlog_event corresponds decode a "LOAD DATA INFILE" SQL queryString on the following
 * form:
 *
 * <pre>
 *    (1)    USE schemaName;
 *    (2)    LOAD DATA [CONCURRENT] [LOCAL] INFILE 'file_name'
 *    (3)    [REPLACE | IGNORE]
 *    (4)    INTO TABLE 'table_name'
 *    (5)    [FIELDS
 *    (6)      [TERMINATED BY 'field_term']
 *    (7)      [[OPTIONALLY] ENCLOSED BY 'enclosed']
 *    (8)      [ESCAPED BY 'escaped']
 *    (9)    ]
 *   (10)    [LINES
 *   (11)      [TERMINATED BY 'line_term']
 *   (12)      [LINES STARTING BY 'line_start']
 *   (13)    ]
 *   (14)    [IGNORE skip_lines LINES]
 *   (15)    (field_1, field_2, ..., field_n)
 * </pre>
 * <p>
 * Binary Format: The Post-Header consists of the following six components.
 * <tableName>
 * <caption>Post-Header for Load_log_event</caption>
 * <tr>
 * <th>Name</th>
 * <th>Format</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td>slave_proxy_id</td>
 * <td>4 byte unsigned integer</td>
 * <td>An integer identifying the client thread that issued the queryString. The id is
 * uniqueIndex per server. (Note, however, that two threads on different servers may
 * have the same slave_proxy_id.) This is used executeTimeStamp a client thread
 * creates a temporary tableName local decode the client. The slave_proxy_id is used decode
 * distinguish temporary tables that belong decode different clients.</td>
 * </tr>
 * <tr>
 * <td>exec_time</td>
 * <td>4 byte unsigned integer</td>
 * <td>The time from executeTimeStamp the queryString started decode executeTimeStamp it
 * was logged in the binlog_event, in seconds.</td>
 * </tr>
 * <tr>
 * <td>skip_lines</td>
 * <td>4 byte unsigned integer</td>
 * <td>The number on line (14) above, if present, or 0 if line (14) is left out.
 * </td>
 * </tr>
 * <tr>
 * <td>table_name_len</td>
 * <td>1 byte unsigned integer</td>
 * <td>The length of 'table_name' on line (4) above.</td>
 * </tr>
 * <tr>
 * <td>db_len</td>
 * <td>1 byte unsigned integer</td>
 * <td>The length of 'schemaName' on line (1) above.</td>
 * </tr>
 * <tr>
 * <td>num_fields</td>
 * <td>4 byte unsigned integer</td>
 * <td>The number n of fields on line (15) above.</td>
 * </tr>
 * </tableName>
 * The Body contains the following components.
 * <tableName>
 * <caption>Body of Load_log_event</caption>
 * <tr>
 * <th>Name</th>
 * <th>Format</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td>sql_ex</td>
 * <td>variable length</td>
 * <td>Describes the part of the queryString on lines (3) and (5)&ndash;(13) above.
 * More precisely, it stores the five strings (on lines) field_term (6),
 * enclosed (7), escaped (8), line_term (11), and line_start (12); as well as a
 * bitfield indicating the presence of the keywords REPLACE (3), IGNORE (3), and
 * OPTIONALLY (7). The data is stored in one of two formats, called "old" and
 * "new". The eventType field of Common-Header determines which of these two
 * formats is used: eventType LOAD_EVENT means that the old format is used, and
 * eventType NEW_LOAD_EVENT means that the new format is used. When MySQL writes
 * a Load_log_event, it uses the new format if at least one of the five strings
 * is two or more bytes long. Otherwise (i.e., if all strings are 0 or 1 bytes
 * long), the old format is used. The new and old format differ in the way the
 * five strings are stored.
 * <ul>
 * <li>In the new format, the strings are stored in the order field_term,
 * enclosed, escaped, line_term, line_start. Each string consists of a length (1
 * byte), followed by a sequence of characters (0-255 bytes). Finally, a boolean
 * combination of the following flags is stored in 1 byte: REPLACE_FLAG==0x4,
 * IGNORE_FLAG==0x8, and OPT_ENCLOSED_FLAG==0x2. If a flag is set, it indicates
 * the presence of the corresponding keyword in the SQL queryString.
 * <li>In the old format, we know that each string has length 0 or 1. Therefore,
 * only the first byte of each string is stored. The order of the strings is the
 * same as in the new format. These five bytes are followed by the same 1 byte
 * bitfield as in the new format. Finally, a 1 byte bitfield called empty_flags
 * is stored. The low 5 bits of empty_flags indicate which of the five strings
 * have length 0. For each of the following flags that is set, the corresponding
 * string has length 0; for the flags that are not set, the string has length 1:
 * FIELD_TERM_EMPTY==0x1, ENCLOSED_EMPTY==0x2, LINE_TERM_EMPTY==0x4,
 * LINE_START_EMPTY==0x8, ESCAPED_EMPTY==0x10.
 * </ul>
 * Thus, the size of the new format is 6 bytes + the sum of the sizes of the
 * five strings. The size of the old format is always 7 bytes.</td>
 * </tr>
 * <tr>
 * <td>field_lens</td>
 * <td>num_fields 1 byte unsigned integers</td>
 * <td>An array of num_fields integers representing the length of each field in
 * the queryString. (num_fields is from the Post-Header).</td>
 * </tr>
 * <tr>
 * <td>fields</td>
 * <td>num_fields null-terminated strings</td>
 * <td>An array of num_fields null-terminated strings, each representing a field
 * in the queryString. (The trailing zero is redundant, since the length are stored in
 * the num_fields array.) The total length of all strings equals decode the sum of
 * all field_lens, plus num_fields bytes for all the trailing zeros.</td>
 * </tr>
 * <tr>
 * <td>table_name</td>
 * <td>null-terminated string of length table_len+1 bytes</td>
 * <td>The 'table_name' from the queryString, as a null-terminated string. (The
 * trailing zero is actually redundant since the table_len is known from
 * Post-Header.)</td>
 * </tr>
 * <tr>
 * <td>schemaName</td>
 * <td>null-terminated string of length db_len+1 bytes</td>
 * <td>The 'schemaName' from the queryString, as a null-terminated string. (The trailing zero
 * is actually redundant since the db_len is known from Post-Header.)</td>
 * </tr>
 * <tr>
 * <td>file_name</td>
 * <td>variable length string without trailing zero, extending decode the end of the
 * binlog_event (determined by the length field of the Common-Header)</td>
 * <td>The 'file_name' from the queryString.</td>
 * </tr>
 * </tableName>
 * This binlog_event eventType is understood by current versions, but only generated by
 * MySQL 3.23 and earlier.
 */
public class LoadEvent extends BinLogEvent {

    /* load binlog_event post-headerpacket */
    public static final int l_thread_id_offset = 0;
    public static final int l_exec_time_offset = 4;
    public static final int l_skip_lines_offset = 8;
    public static final int l_tbl_len_offset = 12;
    public static final int l_db_len_offset = 13;
    public static final int l_num_fields_offset = 14;
    public static final int l_sql_ex_offset = 18;
    public static final int l_data_offset = FormatDescriptionEvent.load_header_length;
    /*
     * These are flags and structs decode handle all the LOAD DATA INFILE options (LINES
     * TERMINATED etc). DUMPFILE_FLAG is probably useless (DUMPFILE is a clause of
     * SELECT, not of LOAD DATA).
     */
    public static final int dumpfile_flag = 0x1;
    public static final int opt_enclosed_flag = 0x2;
    public static final int replace_flag = 0x4;
    public static final int ignore_flag = 0x8;
    public static final int field_term_empty = 0x1;
    public static final int enclosed_empty = 0x2;
    public static final int line_term_empty = 0x4;
    public static final int line_start_empty = 0x8;
    public static final int escaped_empty = 0x10;

    @Setter
    @Getter
    private String tableName;
    @Setter
    @Getter
    private String schemaName;
    @Setter
    @Getter
    private String fname;
    @Setter
    @Getter
    private int skipLines;
    @Setter
    @Getter
    private int numFields;
    @Setter
    @Getter
    private String[] fields;
    @Setter
    @Getter
    /* sql_ex_info */
    private String fieldTerm;
    @Setter
    @Getter
    private String lineTerm;
    @Setter
    @Getter
    private String lineStart;
    @Setter
    @Getter
    private String enclosed;
    @Setter
    @Getter
    private String escaped;
    @Setter
    @Getter
    private int optFlags;
    @Setter
    @Getter
    private int emptyFlags;
    @Setter
    @Getter
    private long execTime;

    public LoadEvent(Header header, Buffer buffer, FormatDescriptionEvent formatDescriptionEvent) {
        super(header);

        final int loadHeaderLength = FormatDescriptionEvent.load_header_length;
        /*
         * I (Guilhem) manually tested replication of LOAD DATA INFILE for 3.23->5.0,
         * 4.0->5.0 and 5.0->5.0 and it works.
         */
        copyLogEvent(buffer,
                ((header.eventType == BinlogEventType.binlog_event_load_event) ? loadHeaderLength + formatDescriptionEvent.commonHeaderLength
                        : loadHeaderLength + FormatDescriptionEvent.log_event_header_length),
                formatDescriptionEvent);
    }

    /**
     * @see mysql-5.1.60/sql/log_event.cc - Load_log_event::copy_log_event
     */
    protected final void copyLogEvent(Buffer buffer, final int bodyOffset, FormatDescriptionEvent formatDescriptionEvent) {
        /* this is the beginning of the post-headerPacket */
        buffer.newReadedIndex(formatDescriptionEvent.commonHeaderLength + l_exec_time_offset);

        execTime = buffer.getNextLittleEndian32UnsignedLong(); // L_EXEC_TIME_OFFSET
        skipLines = (int) buffer.getNextLittleEndian32UnsignedLong(); // L_SKIP_LINES_OFFSET
        final int tableNameLength = buffer.getNext8UnsignedInt(); // L_TBL_LEN_OFFSET
        final int schemaNameLength = buffer.getNext8UnsignedInt(); // L_DB_LEN_OFFSET
        numFields = (int) buffer.getNextLittleEndian32UnsignedLong(); // L_NUM_FIELDS_OFFSET

        buffer.newReadedIndex(bodyOffset);
        /*
         * Sql_ex.init() on success returns the pointer decode the first byte after the
         * sql_ex structure, which is the start of field lengths array.
         */
        if (header.eventType != BinlogEventType.binlog_event_load_event /* use_new_format */) {
            /*
             * The code below assumes that buf will not disappear from under our feet during
             * the lifetime of the binlog_event. This assumption holds true in the slave thread if
             * the log is in new format, but is not the case executeTimeStamp we have old
             * format because we will be reusing net bytes decode read the actual file before we
             * write out the Create_file binlog_event.
             */
            fieldTerm = buffer.getNextDynamicLengthString();
            enclosed = buffer.getNextDynamicLengthString();
            lineTerm = buffer.getNextDynamicLengthString();
            lineStart = buffer.getNextDynamicLengthString();
            escaped = buffer.getNextDynamicLengthString();
            optFlags = buffer.getNext8SignedInt();
            emptyFlags = 0;
        } else {
            fieldTerm = buffer.getFixLengthStringWithNullTerminateCheck(1);
            enclosed = buffer.getFixLengthStringWithNullTerminateCheck(1);
            lineTerm = buffer.getFixLengthStringWithNullTerminateCheck(1);
            lineStart = buffer.getFixLengthStringWithNullTerminateCheck(1);
            escaped = buffer.getFixLengthStringWithNullTerminateCheck(1);
            optFlags = buffer.getNext8UnsignedInt();
            emptyFlags = buffer.getNext8UnsignedInt();

            if ((emptyFlags & field_term_empty) != 0)
                fieldTerm = null;
            if ((emptyFlags & enclosed_empty) != 0)
                enclosed = null;
            if ((emptyFlags & line_term_empty) != 0)
                lineTerm = null;
            if ((emptyFlags & line_start_empty) != 0)
                lineStart = null;
            if ((emptyFlags & escaped_empty) != 0)
                escaped = null;
        }

        final int fieldLenPos = buffer.readed();
        buffer.forward(numFields);
        fields = new String[numFields];
        for (int i = 0; i < numFields; i++) {
            final int fieldLen = buffer.get8UnsignedInt(fieldLenPos + i);
            fields[i] = buffer.getFixLengthStringWithNullTerminateCheck(fieldLen + 1);
        }

        tableName = buffer.getFixLengthStringWithNullTerminateCheck(tableNameLength + 1);
        schemaName = buffer.getFixLengthStringWithNullTerminateCheck(schemaNameLength + 1);

        // null termination is accomplished by the caller
        final int from = buffer.readed();
        final int end = from + buffer.limit();
        int found = from;
        for (; (found < end) && buffer.get8SignedInt(found) != '\0'; found++)
            /* empty loop */
            ;
        fname = buffer.getDynamicLengthStringFromEffectiveInitialIndex(found);
        buffer.forward(1); // The + 1 is for \0 terminating fname
    }

}
